import java.io.*;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Calendar;
import java.util.Date;
import java.util.StringTokenizer;
import java.util.Hashtable;
import java.util.HashMap;
import sun.misc.BASE64Decoder;


/**
 * <p>Title: Response.java</p>
 *
 * <p>Description: Used to process incoming requests to the server and generate
 * the appropriate response to that request. This is where most of the server
 * processing is done, thus making it a very important class to the
 * implementation of your server.</p>
 *
 * <p>Copyright: Copyright (c) 2011</p>
 *
 * <p>Company: </p>
 * @author David Beaver and Aishwarya Iyer
 * @version 1.0
 */
public class Response{
    
    private int bufferSize;
    private int statusCode;
    private int contentLength;
    private String lastModDate;
    private boolean isCGI;
    private byte[] outputBuffer;
    private String status;
    private HttpdConf serverConfig;
    private Request request;
    private Socket client;
    private String cgiOutput;

    /**
    * Default constructor for the response object. Variables are reset and/or
    * intialized here. These variables will be used throughout request processing
    * and response generation.
    */
    public Response() {
        this.status  =  "";
        this.contentLength = 0;
        this.statusCode = 0;
        this.bufferSize = 1000000;
        this.outputBuffer = new byte[this.bufferSize];
        this.isCGI = false;
    }

    public Response(HttpdConf configuration, Socket client){
        this.serverConfig = configuration;
        this.status =  "";
        this.contentLength = 0;
        this.statusCode = 0;
        this.bufferSize = 100000000;
        this.outputBuffer = new byte[this.bufferSize];
        this.client = client;
        this.isCGI = false;
    }

    /**
    * Used to process the request that came from the client to the server. There
    * are many things that need to be checked and verified during the processing
    * of a request. You will need to check for authentication, errors, cgi
    * scripts, type of request, etc, and handle each condition appropriately.
    *
    * HINT: it helps to use boolean flags throughout your code to check for the
    * various conditions that may or may not occur.
    *
    * @param myRequest Request object that was generated and stores all the
    *   request information that came in from the client
    * @param MIMETable Hashtable which contains the various mime types the
    *   server can handle.
    * @param env Environment object that contains the environment variables
    *   necessary for cgi script execution.
    * public void processRequest(Request myRequest, Hashtable MIMETable, Environment env)
    */
    public void processRequest(Request myRequest) throws InterruptedException{ //********get rid of throw exception*********
        this.request = myRequest;
        String URI = myRequest.getURI();
        String realPath = this.serverConfig.solveAlias(URI);
        StringTokenizer extensionParser = new StringTokenizer(realPath, "."); //send the file URI through the parser
        //need to find filepath and the extension
        String fileName = "";
        if(extensionParser.countTokens() < 3){ //if only one period is in the file path
            fileName +=  extensionParser.nextToken(".");
        }else{ //the file path contains multiple periods
            for(int count = 0; count < extensionParser.countTokens(); count++){ //ignore early periods
                fileName +=  extensionParser.nextToken(".");
                if(count != extensionParser.countTokens()){ //if this is the last period filepath found
                    fileName += ".";
                }
            }
        }
        
        String extension = "";
        if(extensionParser.hasMoreTokens()){ //if an extension was found
            extension = extensionParser.nextToken(); //save the extension
            extension = extension.replace('/', ' ').trim();
        }else if(myRequest.getMethod().equals("GET") || myRequest.getMethod().equals("HEAD")){ //if an extension wasnt found and the method was HEAD or GET
            //error 400
            this.statusCode = 400;
            this.setStatus(400);
        }
        File requestedFile;
        FileInputStream fileContents;
        if(this.checkMIME(this.serverConfig.getMIME(), extension)){ //check if server can support requested file type
            try{
                String path = fileName + "." + extension;
                if(extension.equals("pl")){
                    this.isCGI = true;
                    Environment cgiEnv = new Environment(this.serverConfig,this.request,this.client);
                    CGIHandler cgi = new CGIHandler(this.request, cgiEnv);
                    this.cgiOutput = cgi.runScript();
                    this.outputBuffer = cgiOutput.getBytes();
                    this.contentLength = this.outputBuffer.length;
                    this.statusCode = 200;
                    this.setStatus(200);
                    return;
                }else if (!this.serverConfig.needsAuthentication(URI)) {
                    requestedFile = new File(fileName + "." + extension); //check if server has requested file
                    /*
                    if(!requestedFile.exists()){
                        this.statusCode = 404;
                        this.setStatus(404);
                        return;
                    }
                     * */

                    long timeLastMod = requestedFile.lastModified();
                    Calendar cacheTime = Calendar.getInstance();
                    //Calendar lastModTime = Calendar.getInstance();
                    Date ifModified = this.request.getCacheDate();
                    long cacheTimeInMillis = 0;
                    Date lastModified = new Date(timeLastMod);
                    this.lastModDate = lastModified.toString();
                    //lastModTime.setTime(lastModified);
                    //int results = -1;
                    if(ifModified != null){
                        //results = ifModified.compareTo(lastModified);
                        cacheTime.setTime(ifModified);
                        cacheTimeInMillis = cacheTime.getTimeInMillis();
                    }
                    //ifmodified < lastmodified
                        //404
                    //ifmodified => lastmodified
                        //301
                    if(ifModified.before(lastModified) == true)
                    { //if file has been modified recently
                        this.contentLength = (int)requestedFile.length(); //save the content length
                        System.out.println("\nOpening: "+ path);
                        fileContents = new FileInputStream(requestedFile);
                        int bytesRead = fileContents.read(this.outputBuffer); //read the file into the buffer
                        if(bytesRead != (int)this.contentLength){
                            this.serverConfig.writeLogFile("**Error**: Couldn't retrieve full content of " + path + "\n");
                            System.out.println("Error retrieving file contents: " + path);
                        }
                        this.statusCode = 200;
                        this.setStatus(200);
                    }else{ //the cached file is ok to use
                        this.statusCode = 304;
                        this.setStatus(304);
                    }
                }else{
                    StringTokenizer encodedParser = new StringTokenizer(this.request.getAuthentication()); //parser the Authorization header
                    if(encodedParser.hasMoreTokens()){ //if an Authorization header was found
                        encodedParser.nextToken(); //removes 'Basic' from header
                        String encodedAuth = encodedParser.nextToken(); //save the base64 endcoded authorization
                        BASE64Decoder decoder = new BASE64Decoder(); //create a decoder
                        byte[] decodedBytes = decoder.decodeBuffer(encodedAuth); //save the decoded the authorization
                        if(this.serverConfig.verifyAuthentication(URI, decodedBytes)){ //check if the authorization is correct
                            requestedFile = new File(path); //check if server has requested file
                            System.out.println("\nOpening: "+ path);
                            this.contentLength = (int)requestedFile.length(); //save the content length
                            fileContents = new FileInputStream(requestedFile); //
                            int bytesRead = fileContents.read(this.outputBuffer); //read the file into the buffer
                            if(bytesRead != (int)this.contentLength){ //if the entire file isn't read
                                this.serverConfig.writeLogFile("**Error**: Couldn't retrieve full contents of " + path + "\n");
                                System.out.println("Error retrieving file contents of " + path); //report error
                            }
                            this.statusCode = 200;
                            this.setStatus(200);
                        }else{ //authorization is incorrect
                            this.statusCode = 403;
                            this.setStatus(statusCode);
                        }
                    }else{ //parser found no Authentication
                        this.statusCode = 401;
                        this.setStatus(statusCode);
                    }

                }
            }catch(NullPointerException nex){
                //file wasn't found
                //error 404
                this.statusCode = 404;
                this.setStatus(404);
            }catch(FileNotFoundException fex){
            }catch(IOException iex){
                System.out.println("IO exception");
            }

        }

    }

    /**
    * Used to output a correctly formatted response to the client. This function
    * will need to process any output from a cgi script as well as generate the
    * appropriate headers and body required by an HTTP response.
    *
    * @param output OutputStream object that will be used to send the response
    *   back to the socket.
    */
    public void writeOutput(OutputStream clientOutput) {
        //build the response for the client
        StringBuilder responseBuilder = new StringBuilder("HTTP/1.1 ");
        responseBuilder.append(this.statusCode).append(" ").append(this.status).append("\n");
        responseBuilder.append("Server: Dash Webserver\n");
        responseBuilder.append("Cache-Control: max-age=10\n");
        if(this.contentLength != 0){
            responseBuilder.append("Content-Length: ").append(this.contentLength).append("\n");
            responseBuilder.append("Last-Modified: ").append(this.lastModDate).append("\n");
        }
        if(this.request.keepAlive()){
            responseBuilder.append("Keep-Alive: timeout=5, max=100\n");
            responseBuilder.append("Connection: Keep-Alive\n");
        }
        if(this.statusCode == 401){
            responseBuilder.append("Keep-Alive: timeout=5, max=100\n");
            responseBuilder.append("Connection: Keep-Alive\n");
            responseBuilder.append("WWW-Authenticate: Basic\n");
            responseBuilder.append("Content-Type: text/html; charset=iso-8859-1").append("\n\n");
        }else if(this.isCGI == true){
            responseBuilder.append("Keep-Alive: timeout=5, max=100\n");
            responseBuilder.append("Connection: Keep-Alive\n");
        }else if (this.statusCode != 403) {
            responseBuilder.append("Content-Type: ").append(this.request.getMediaType()).append("\n");
        }
        //the following should only happen if the method requires a body
        try {
            String response = "";
            if(this.isCGI){
                this.serverConfig.writeLogFile("RESPONSE:\n");
                this.serverConfig.writeLogFile(responseBuilder.toString()+"\n");
                this.serverConfig.writeLogFile("------------------------------------------------------------------------------------------------------\n");
                responseBuilder.append(this.cgiOutput);
                response = responseBuilder.toString();
                clientOutput.write(response.getBytes());
            }else{
                responseBuilder.append("\n");
                response = responseBuilder.toString();
                this.serverConfig.writeLogFile("RESPONSE:\n");
                this.serverConfig.writeLogFile(response.trim() + "\n");
                this.serverConfig.writeLogFile("------------------------------------------------------------------------------------------------------\n");
                clientOutput.write(response.getBytes());
                if(this.outputBuffer.length > 0){
                    clientOutput.write(this.outputBuffer,0,this.contentLength);
                }
                //response += new String(this.outputBuffer);
            }
            System.out.println(response);
            System.out.println("------------------------------------------------------------------------------------------");
        } catch (IOException ex) {
            this.serverConfig.writeLogFile("**Error**: Sending response to " + this.request.getURI() +  "\n");
            System.out.println(this.request.getURI() + " had an error while writing output");
        }
    }

    /**
    * Used to test for authentication. If the .htaccess file shows that
    * authentication is needed for access to the file or directory then set the
    * appropriate headers and set the appropriate status codes unless the user
    * has included their authentication. If this is the case, check to make sure
    * their authentication is valid.
    *
    * @param req Request object which is needed to check for authentication
    */
    //Example path: /docs/usr/pass.txt
    //******************** this function requires fixing ***********************
    //*******This should be adjusted to find the system's file separator********
    public boolean checkAuthentication(Request clientRequest){
        String path = clientRequest.getURI();
        StringTokenizer directoryParser = new StringTokenizer(path, "/"); //parse the path at every new directory found
        String currentDirectory = "/"; //the current directory is the root
        File htaccess = new File(currentDirectory + ".htaccess"); //check for the existence of the .htaccess file
        while(!htaccess.exists() && directoryParser.hasMoreTokens()){ //if the file doesn't exist in this folder and there are more directories to check
            currentDirectory += directoryParser.nextToken() + "/"; //move into the next directory
            htaccess = new File(currentDirectory + ".httaccess"); //check for the existence of the .htaccess file
            if(htaccess.exists()){ //if the .htaccess file exists
                return true; //return true
            }
        }
        //traversed all directories
        return false; //return false
    }


    /**
    * Used to set the reason for each HTTP status code as designated by the
    * protocol.
    *
    * @param code int value which corresponds to each status code
    */
    public void setStatus(int code) {
        switch(code){
            case 200:
                this.status = "OK";
                break;
            case 400:
                this.status = "Bad Request";
                break;
            case 401:
                this.status = "Unauthorized";
                break;
            case 403:
                this.status = "Forbidden";
                break;
            case 404:
                this.status = "Not Found";
                break;
            default:
                this.status = "Invalid Code";
                break;
        }
    }

    /**
    * Private function used to return the appropriate mime type for the file that
    * is being requested
    *
    * @param MIMETable Hashtable of mime types from your mime.types file
    * @param extension String value which designates the extension of the file
    *   being requested. This will be used to determine the mime type
    * @return String value that contains the mime type of the file
    */
    private String getMIME(HashMap MIMETable, String extension) {
        return MIMETable.get(extension).toString();
    }

    /**
    * Private function used to determine whether the mime type requested is a
    * valid mime type
    *
    * @param MIMETable Hashtable value of the available mime types as designated
    *   by the mime.types file
    * @param extension String value which consists of the extension type
    *   requested. Used to determine the correct mime type
    * @return true if mime type if valid, false otherwise
    */
    private boolean checkMIME(HashMap MIMETable, String extension) {
        return MIMETable.containsKey(extension);
    }

    /**
    * private function used when processing a request from the client. Here, you
    * will check for mime type validity and handle a put request if it is
    * requested. If the request is PUT, you will need to use the body of the
    * request to modify the existing file.
    *
    * @param MIMETable Hashtable that contains the valid mime types as
    *   determined by the mime.types file
    * @param body String value that contains the body of the request.
    */
    private void processWithExistence(Hashtable MIMETable, String body) {}

    /**
    * Private function specifically used to handle output from a cgi script. You
    * will need to check the header passed back from the cgi script to determine
    * the status code of the response. From there, add your headers, attach the
    * body and add any other server directives that need to be included.
    *
    * @param dataOut BufferedOutputStream object that will write to the client
    */
    private void processCGI(BufferedOutputStream dataOut) {}

    /**
    * Used to write the appropriate information to the log file.
    *
    * @param logPath String value which contains the location of your log file
    * @param host String value that contains the address of the client who made
    *   the request
    */
    public void writeToLog(String logPath, String host) {}
}

